Chapter 5. Getting to Know the Hardware

hardware n. The part of a computer system that can be kicked.

As an embedded software engineer, you'll have the opportunity to work with many different pieces of hardware in your career. In this chapter, I will teach you a simple procedure that I use to familiarize myself with any new board. In the process, I'll guide you through the creation of a header file that describes the board's most important features and a piece of software that initializes the hardware to a known state.

5.1 Understand the Big Picture

Before writing software for an embedded system, you must first be familiar with the hardware on which it will run. At first, you just need to understand the general operation of the system. You do not need to understand every little detail of the hardware; that kind of knowledge will not be needed right away and will come with time.

Whenever you receive a new board, you should take some time to read whatever documents have been provided with it. If the board is an off-the-shelf product, it might arrive with a "User's Guide" or "Programmer's Manual" that has been written with the software developer in mind. However, if the board was custom designed for your project, the documentation might be more cryptic or written mainly for the reference of the hardware designers. Either way, this is the single best place for you to start.

While you are reading the documentation, set the board itself aside. This will help you to focus on the big picture. There will be plenty of time to examine the actual board more closely when you have finished reading. Before picking up the board, you should be able to answer two basic questions about it:

For example, imagine that you are a member of a modem design team. You are a software developer who has just received an early prototype board from the hardware designers. Because you are already familiar with modems, the overall purpose of the board and the data-flow through it should be fairly obvious to you. The purpose of the board is to send and receive digital data over an analog telephone line. The hardware reads digital data from one set of electrical connections and writes an analog version of the data to an attached telephone line. Data also flows in the opposite direction, when analog data is read from the telephone line jack and output digitally.

Though the purpose of most systems is fairly obvious, the flow of the data might not be. I often find that a data-flow diagram is helpful in achieving rapid comprehension. If you are lucky, the documentation provided with your hardware will contain a superset of the block diagram you need. However, you might still find it useful to create your own data-flow diagram. That way, you can leave out those hardware components that are unrelated to the basic flow of data through the system.

In the case of the Arcom board, the hardware was not designed with a particular application in mind. So for the remainder of this chapter, we'll have to imagine that it does have a purpose. We shall assume the board was designed for use as a printer-sharing device. A printer-sharing device allows two computers to share a single printer. The user of the device connects one computer to each serial port and a printer to the parallel port. Both computers can then send documents to the printer, though only one of them can do so at a given time.

In order to illustrate the flow of data through the printer-sharing device, I've drawn the diagram in Figure 5-1. (Only those hardware devices that are involved in this application of the Arcom board are shown.) By looking at the block diagram, you should be able to quickly visualize the flow of the data through the system. Data to be printed is accepted from either serial port, held in RAM until the printer is ready for more data, and delivered to the printer via the parallel port. The software that makes all of this happen is stored in ROM.

Figure 5-1. Data-flow diagram for the printer-sharing device

figs/ESP_0501.gif

Once you've created a block diagram, don't just crumple it up and throw it away. You should instead put it where you can refer to it throughout the project. I recommend creating a project notebook or binder, with this data-flow diagram on the first page. As you continue working with this piece of hardware, write down everything you learn about it in your notebook. You might also want to keep notes about the software design and implementation. A project notebook is valuable not only while you are developing the software, but also once the project is complete. You will appreciate the extra effort you put into keeping a notebook when you need to make changes to your software, or work with similar hardware, months or years later.

If you still have any big-picture questions after reading the hardware documents, ask a hardware engineer for some help. If you don't already know the hardware's designer, take a few minutes to introduce yourself. If you have some time, take him out to lunch or buy him a beer after work. (You don't even have to talk about the project the whole time!) I have found that many software engineers have difficulty communicating with hardware engineers, and vice versa. In embedded systems development, it is especially important that the hardware and software teams be able to communicate with one another.

5.2 Examine the Landscape

It is often useful to put yourself in the processor's shoes for a while. After all, the processor is only going to do what you ultimately instruct it to do with your software. Imagine what it is like to be the processor: what does the processor's world look like? If you think about it from this perspective, one thing you quickly realize is that the processor has a lot of compatriots. These are the other pieces of hardware on the board, with which the processor can communicate directly. In this section you will learn to recognize their names and addresses.

The first thing to notice is that there are two basic types: memories and peripherals. Obviously, memories are for data and code storage and retrieval. But you might be wondering what the peripherals are. These are specialized hardware devices that either coordinate interaction with the outside world (I/O) or perform a specific hardware function. For example, two of the most common peripherals in embedded systems are serial ports and timers. The former is an I/O device, and the latter is basically just a counter.

Members of Intel's 80x86 and some other processor families have two distinct address spaces through which they can communicate with these memories and peripherals. The first address space is called the memory space and is intended mainly for memory devices; the second is reserved exclusively for peripherals and is called the I/O space. However, peripherals can also be located within the memory space, at the discretion of the hardware designer. When that happens, we say that those peripherals are memory-mapped.

From the processor's point of view, memory-mapped peripherals look and act very much like memory devices. However, the function of a peripheral is obviously quite different from that of a memory. Instead of simply storing the data that is provided to it, a peripheral might instead interpret it as a command or as data to be processed in some way. If peripherals are located within the memory space, we say that the system has memory-mapped I/O.

The designers of embedded hardware often prefer to use memory-mapped I/O exclusively, because it has advantages for both the hardware and software developers. It is attractive to the hardware developer because he might be able to eliminate the I/O space, and some of its associated wires, altogether. This might not significantly reduce the production cost of the board, but it might reduce the complexity of the hardware design. Memory-mapped peripherals are also better for the programmer, who is able to use pointers, data structures, and unions to interact with the peripherals more easily and efficiently.[1]

5.2.1 Memory Map

All processors store their programs and data in memory. In some cases this memory resides on the very same chip as the processor, but more often it is located in external memory chips. These chips are located in the processor's memory space, and the processor communicates with them by way of two sets of electrical wires called the address bus and the data bus. To read or write a particular location in memory, the processor first writes the desired address onto the address bus. The data is then transferred over the data bus.

As you are reading about a new board, create a table that shows the name and address range of each memory device and peripheral that is located in the memory space. Organize the table so that the lowest address is at the bottom and the highest address is at the top. Each time you add a device to the memory map, place it in its approximate location in memory and label the starting and ending addresses, in hexadecimal. After you have finished inserting all of the devices into the memory map, be sure to label any unused memory regions as such.

If you look back at the block diagram of the Arcom board in Figure 5-1, you will see that there are three devices attached to the address and data buses. These devices are the RAM and ROM and a mysterious device labeled "Zilog 85230 Serial Controller." The documentation provided by Arcom says that the RAM is located at the bottom of memory and extends upward for the first 128 KB of the memory space. The ROM is located at the top of memory and extends downward for 256 KB. But this area of memory actually contains two ROMs—an EPROM and a Flash memory device—each of size 128 KB. The third device, the Zilog 85230 Serial Communications Controller, is a memory-mapped peripheral whose registers are accessible between the addresses 70000h and 72000h.

The memory map in Figure 5-2 shows what these devices look like to the processor. In a sense, this is the processor's "address book." Just as you maintain a list of names and addresses in your personal life, you must maintain a similar list for the processor. The memory map contains one entry for each of the memories and peripherals that are accessible from the processor's memory space. This diagram is arguably the most important piece of information about the system and should be kept up-to-date and as part of the permanent records associated with the project.

Figure 5-2. Memory map for the Arcom board

figs/ESP_0502.gif

For each new board, you should create a header file that describes its most important features. This file provides an abstract interface to the hardware. In effect, it allows you to refer to the various devices on the board by name, rather than by address. This has the added benefit of making your application software more portable. If the memory map ever changes—for example, if the 128 KB of RAM is moved—you need only change the affected lines of the board-specific header file and recompile your application.

As this chapter progresses, I will show you how to create a header file for the Arcom board. The first section of this file is listed below. The part of the header file below describes the memory map. The most notable difference between the memory map in the header file and that in Figure 5-2 is the format of the addresses. Pointers Versus Addresses explains why.

/**********************************************************************
 *
 *  Memory Map
 *
 *            Base Address   Size  Description
 *            -------------- ----- -----------------------------------
 *            0000:0000h     128K  SRAM
 *            2000:0000h           Unused
 *            7000:0000h           Zilog SCC Registers
 *            7000:1000h           Zilog SCC Interrupt Acknowledge
 *            7000:2000h           Unused
 *            C000:0000h     128K  Flash
 *            E000:0000h     128K  EPROM
 *
 **********************************************************************/

#define SRAM_BASE       (void *) 0x00000000
#define SCC_BASE        (void *) 0x70000000
#define SCC_INTACK      (void *) 0x70001000
#define FLASH_BASE      (void *) 0xC0000000
#define EPROM_BASE      (void *) 0xE0000000

Pointers Versus Addresses

In both C and C++, the value of a pointer is an address. So when we say that we have a pointer to some data, we really mean that we have the address at which the data is stored. But programmers don't usually set or examine these addresses directly. The exception to this rule are the developers of operating systems, device drivers, and embedded software, who sometimes need to set the value of a pointer explicitly in their code.

Unfortunately, the exact representation of an address can change from processor to processor or can even be compiler dependent. This means that a physical address like 12345h might not be stored in exactly that form, or might even be stored differently by different compilers.[2] The issue that then arises is how a programmer can set the value of a pointer explicitly so that it points to the desired location in the memory map.

Most C/C++ compilers for 80x86 processors use 32-bit pointers. However, the older processors don't have a simple linear 32-bit address space. For example, Intel's 80188EB processor has only a 20-bit address space. And, in addition, none of its internal registers can hold more than 16 bits. So on this processor, two 16-bit registers—a segment register and an offset register—are combined to create the 20-bit physical address. (The physical address computation involves left-shifting the contents of the segment register by four bits and adding the contents of the offset register to the result. Any overflow into the 21st bit is ignored.)

To declare and initialize a pointer to a register located at physical address 12345h we therefore write:

int * pRegister = (int *) 0x10002345; 

where the leftmost 16 bits contain the segment value and the rightmost 16 bits contain the offset value.

For convenience, 80x86 programmers sometimes write addresses as segment:offset pairs. Using this notation, the physical address 12345h would be written as 0x1000:2345. This is precisely the value—sans colon—that we used to initialize the pointer above. However, for each possible physical address there are 4096 distinct segment:offset pairs that point to a given physical address. For example, the pairs 0x1200:0345 and 0x1234:0005 (and 4093 others) also refer to physical address 12345h.

5.2.2 I/O Map

If a separate I/O space is present, it will be necessary to repeat the memory map exercise to create an I/O map for the board as well. The process is exactly the same. Simply create a table of peripheral names and address ranges, organized in such a way that the lowest addresses are at the bottom. Typically, a large percentage of the I/O space will be unused because most of the peripherals located there will have only a handful of registers.

The I/O map for the Arcom board is shown in Figure 5-3. It includes three devices: the peripheral control block (PCB), parallel port, and debugger port. The PCB is a set of registers within the 80188EB that are used to control the on-chip peripherals. The chips that control the parallel port and debugger port reside outside of the processor. These ports are used to communicate with the printer and a host-based debugger, respectively.

Figure 5-3. I/O map for the Arcom board

figs/ESP_0503.gif

The I/O map is also useful when creating the header file for your board. Each region of the I/O space maps directly to a constant, called the base address. The translation of the above I/O map into a set of constants can be found in the following listing:

/**********************************************************************
 *
 *  I/O Map
 *
 *            Base Address    Description
 *            --------------- ----------------------------------------
 *            0000h           Unused
 *            FC00h           SourceVIEW Debugger Port (SVIEW)
 *            FD00h           Parallel I/O Port (PIO)
 *            FE00h           Unused
 *            FF00h           Peripheral Control Block (PCB)
 *
 **********************************************************************/

#define SVIEW_BASE   0xFC00
#define PIO_BASE     0xFD00
#define PCB_BASE     0xFF00

5.3 Learn How to Communicate

Now that you know the names and addresses of the memory and peripherals attached to the processor, it is time to learn how to communicate with the latter. There are two basic communication techniques: polling and interrupts. In either case, the processor usually issues some sort of commands to the device—by way of the memory or I/O space—and waits for the device to complete the assigned task. For example, the processor might ask a timer to count down from 1000 to 0. Once the countdown begins, the processor is interested in just one thing: is the timer finished counting yet?

If polling is used, then the processor repeatedly checks to see if the task has been completed. This is analogous to the small child who repeatedly asks "are we there yet?" throughout a long trip. Like the child, the processor spends a large amount of otherwise useful time asking the question and getting a negative response. To implement polling in software, you need only create a loop that reads the status register of the device in question. Here is an example:

    do
    {
        // Play games, read, listen to music, etc.
        ...

        // Poll to see if we're there yet.
        status = areWeThereYet();

    } while (status == NO);

The second communication technique uses interrupts. An interrupt is an asynchronous electrical signal from a peripheral to the processor. When interrupts are used, the processor issues commands to the peripheral exactly as before, but then waits for an interrupt to signal completion of the assigned work. While the processor is waiting for the interrupt to arrive, it is free to continue working on other things. When the interrupt signal is finally asserted, the processor temporarily sets aside its current work and executes a small piece of software called the interrupt service routine (ISR). When the ISR completes, the processor returns to the work that was interrupted.

Of course, this isn't all automatic. The programmer must write the ISR himself and "install" and enable it so that it will be executed when the relevant interrupt occurs. The first few times you do this, it will be a significant challenge. But, even so, the use of interrupts generally decreases the complexity of one's overall code by giving it a better structure. Rather than device polling being embedded within an unrelated part of the program, the two pieces of code remain appropriately separate.

On the whole, interrupts are a much more efficient use of the processor than polling. The processor is able to use a larger percentage of its waiting time to perform useful work. However, there is some overhead associated with each interrupt. It takes a good bit of time—relative to the length of time it takes to execute an opcode—to put aside the processor's current work and transfer control to the interrupt service routine. Many of the processor's registers must be saved in memory, and lower-priority interrupts must be disabled. So in practice both methods are used frequently. Interrupts are used when efficiency is paramount or multiple devices must be monitored simultaneously. Polling is used when the processor must respond to some event more quickly than is possible using interrupts.

5.3.1 Interrupt Map

Most embedded systems have only a handful of interrupts. Associated with each of these are an interrupt pin (on the outside of the processor chip) and an ISR. In order for the processor to execute the correct ISR, a mapping must exist between interrupt pins and ISRs. This mapping usually takes the form of an interrupt vector table. The vector table is usually just an array of pointers to functions, located at some known memory address. The processor uses the interrupt type (a unique number associated with each interrupt pin) as its index into this array. The value stored at that location in the vector table is usually just the address of the ISR to be executed.[3]

It is important to initialize the interrupt vector table correctly. (If it is done incorrectly, the ISR might be executed in response to the wrong interrupt or never executed at all.) The first part of this process is to create an interrupt map that organizes the relevant information. An interrupt map is a table that contains a list of interrupt types and the devices to which they refer. This information should be included in the documentation provided with the board. Table 5-1 shows the interrupt map for the Arcom board.

Table 5-1. Interrupt Map for the Arcom Board

Interrupt Type

Generating Device

8

Timer/Counter #0

17

Zilog 85230 SCC

18

Timer/Counter #1

19

Timer/Counter #2

20

Serial Port Receive

21

Serial Port Transmit

Once again, our goal is to translate the information in the table into a form that is useful for the programmer. After constructing an interrupt map like the one above, you should add a third section to the board-specific header file. Each line of the interrupt map becomes a single #define within the file, as shown:

/**********************************************************************
 *
 *  Interrupt Map
 *
 **********************************************************************/

/*
 * Zilog 85230 SCC
 */
#define SCC_INT         17

/*
 * On-Chip Timer/Counters
 */
#define TIMER0_INT       8
#define TIMER1_INT      18
#define TIMER2_INT      19

/*
 * On-Chip Serial Ports
 */
#define RX_INT          20
#define TX_INT          21

5.4 Get to Know the Processor

If you haven't worked with the processor on your board before, you should take some time to get familiar with it now. This shouldn't take very long if you do all of your programming in C or C++. To the user of a high-level language, most processors look and act pretty much the same. However, if you'll be doing any assembly language programming, you will need to familiarize yourself with the processor's architecture and basic instruction set.

Everything you need to know about the processor can be found in the databooks provided by the manufacturer. If you don't have a databook or programmer's guide for your processor already, you should obtain one immediately. If you are going to be a successful embedded systems programmer, you must be able to read databooks and get something out of them. Processor databooks are usually well written—as databooks go—so they are an ideal place to start. Begin by flipping through the databook and noting the sections that are most relevant to the tasks at hand. Then go back and begin reading the processor overview section.

5.4.1 Processors in General

Many of the most common processors are members of families of related devices. In some cases, the members of such a processor family represent points along an evolutionary path. The most obvious example is Intel's 80x86 family, which spans from the original 8086 to the Pentium II—and beyond. In fact, the 80x86 family has been so successful that it has spawned an entire industry of imitators.

As it is used in this book, the term processor refers to any of three types of devices known as microprocessors, microcontrollers, and digital signal processors. The name microprocessor is usually reserved for a chip that contains a powerful CPU that has not been designed with any particular computation in mind. These chips are usually the foundation of personal computers and high-end workstations. The most common microprocessors are members of Motorola's 68k—found in older Macintosh computers—and the ubiquitous 80x86 families.

A microcontroller is very much like a microprocessor, except that it has been designed specifically for use in embedded systems. Microcontrollers typically include a CPU, memory (a small amount of RAM, ROM, or both), and other peripherals in the same integrated circuit. If you purchase all of these items on a single chip, it is possible to reduce the cost of an embedded system substantially. Among the most popular microcontrollers are the 8051 and its many imitators and Motorola's 68HCxx series. It is also common to find microcontroller versions of popular microprocessors. For example, Intel's 386EX is a microcontroller version of the very successful 80386 microprocessor.

The final type of processor is a digital signal processor, or DSP. The CPU within a DSP is specially designed to perform discrete-time signal processing calculations—like those required for audio and video communications—extremely fast. Because DSPs can perform these types of calculations much faster than other processors, they offer a powerful, low-cost microprocessor alternative for designers of modems and other telecommunications and multimedia equipment. Two of the most common DSP families are the TMS320Cxx and 5600x series from TI and Motorola, respectively.

5.4.2 Intel's 80188EB Processor

The processor on the Arcom board is an Intel 80188EB—a microcontroller version of the 80186. In addition to the CPU, the 80188EB contains an interrupt control unit, two programmable I/O ports, three timer/counters, two serial ports, a DRAM controller, and a chip-select unit. These extra hardware devices are located within the same chip and are referred to as on-chip peripherals. The CPU is able to communicate with and control the on-chip peripherals directly, via internal buses.

Although the on-chip peripherals are distinct hardware devices, they act like little extensions of the 80186 CPU. The software can control them by reading and writing a 256-byte block of registers known as the peripheral control block (PCB). You may recall that we encountered this block when we first discussed the memory and I/O maps for the board. By default the PCB is located in the I/O space, beginning at address FF00h. However, if so desired, the PCB can be relocated to any convenient address in either the I/O or memory space.

The control and status registers for each of the on-chip peripherals are located at fixed offsets from the PCB base address. The exact offset of each register can be found in a table in the 80188EB Microprocessor User's Manual. To isolate these details from your application software, it is good practice to include the offsets of any registers you will be using in the header file for your board. I have done this for the Arcom board, but only those registers that will be discussed in later chapters of the book are shown here:

/**********************************************************************
 *
 *  On-Chip Peripherals
 *
 **********************************************************************/

/*
 * Interrupt Control Unit
 */
#define EOI     (PCB_BASE + 0x02)
#define POLL    (PCB_BASE + 0x04)
#define POLLSTS (PCB_BASE + 0x06)

#define IMASK   (PCB_BASE + 0x08)
#define PRIMSK  (PCB_BASE + 0x0A)

#define INSERV  (PCB_BASE + 0x0C)
#define REQST   (PCB_BASE + 0x0E)
#define INSTS   (PCB_BASE + 0x10)

/*
 * Timer/Counters
 */
#define TCUCON  (PCB_BASE + 0x12)

#define T0CNT   (PCB_BASE + 0x30)
#define T0CMPA  (PCB_BASE + 0x32)
#define T0CMPB  (PCB_BASE + 0x34)
#define T0CON   (PCB_BASE + 0x36)

#define T1CNT   (PCB_BASE + 0x38)
#define T1CMPA  (PCB_BASE + 0x3A)
#define T1CMPB  (PCB_BASE + 0x3C)
#define T1CON   (PCB_BASE + 0x3E)

#define T2CNT   (PCB_BASE + 0x40)
#define T2CMPA  (PCB_BASE + 0x42)
#define T2CON   (PCB_BASE + 0x46)

/*
 * Programmable I/O Ports
 */
#define P1DIR   (PCB_BASE + 0x50)
#define P1PIN   (PCB_BASE + 0x52)
#define P1CON   (PCB_BASE + 0x54)
#define P1LTCH  (PCB_BASE + 0x56)

#define P2DIR   (PCB_BASE + 0x58)
#define P2PIN   (PCB_BASE + 0x5A)
#define P2CON   (PCB_BASE + 0x5C)
#define P2LTCH  (PCB_BASE + 0x5E)

Other things you'll want to learn about the processor from its databook are:

5.5 Study the External Peripherals

At this point, you've studied every aspect of the new hardware except the external peripherals. These are the hardware devices that reside outside the processor chip and communicate with it by way of interrupts and I/O or memory-mapped registers.

Begin by making a list of the external peripherals. Depending on your application, this list might include LCD or keyboard controllers, A/D converters, network interface chips, or custom ASICs (Application-Specific Generated Circuits). In the case of the Arcom board, the list contains just three items: the Zilog 85230 Serial Controller, parallel port, and debugger port.

You should obtain a copy of the user's manual or databook for each device on your list. At this early stage of the project, your goal in reading these documents is to understand the basic functions of the device. What does the device do? What registers are used to issue commands and receive the results? What do the various bits and larger fields within these registers mean? When, if ever, does the device generate interrupts? How are interrupts acknowledged or cleared at the device?

When you are designing the embedded software, you should try to break the program down along device lines. It is usually a good idea to associate a software module called a device driver with each of the external peripherals. This is nothing more than a collection of software routines that control the operation of the peripheral and isolate the application software from the details of that particular hardware device. I'll have a lot more to say about device drivers in Chapter 7.

5.6 Initialize the Hardware

The final step in getting to know your new hardware is to write some initialization software. This is your best opportunity to develop a close working relationship with the hardware—especially if you will be developing the remainder of the software in a high-level language. During hardware initialization it will be impossible to avoid using assembly language. However, after completing this step, you will be ready to begin writing small programs in C or C++.[4]

If you are one of the first software engineers to work with a new board—especially a prototype—the hardware might not work as advertised. All processor-based boards require some amount of software testing to confirm the correctness of the hardware design and the proper functioning of the various peripherals. This puts you in an awkward position when something is not working properly. How do you know if the hardware or your software is to blame? If you happen to be good with hardware or have access to a simulator, you might be able to construct some experiments to answer this question. Otherwise, you should probably ask a hardware engineer to join you in the lab for a joint debugging session.

The hardware initialization should be executed before the startup code described in Chapter 3. The code described there assumes that the hardware has already been initialized and concerns itself only with creating a proper runtime environment for high-level language programs. Figure 5-4 provides an overview of the entire initialization process, from processor reset through hardware initialization and C/C++ startup code to main.

Figure 5-4. The hardware and software initialization process

figs/ESP_0504.gif

The first stage of the initialization process is the reset code. This is a small piece of assembly (usually only two or three instructions) that the processor executes immediately after it is powered on or reset. The sole purpose of this code is to transfer control to the hardware initialization routine. The first instruction of the reset code must be placed at a specific location in memory, usually called the reset address, that is specified in the processor databook. The reset address for the 80188EB is FFFF0h.

Most of the actual hardware initialization takes place in the second stage. At this point, we need to inform the processor about its environment. This is also a good place to initialize the interrupt controller and other critical peripherals. Less critical hardware devices can be initialized when the associated device driver is started, usually from within main.

Intel's 80188EB has several internal registers that must be programmed before any useful work can be done with the processor. These registers are responsible for setting up the memory and I/O maps and are part of the processor's internal chip-select unit. By programming the chip-select registers, you are essentially waking up each of the memory and I/O devices that are connected to the processor. Each chip-select register is associated with a single "chip enable" wire that runs from the processor to some other chip. The association between particular chip-selects and hardware devices must be established by the hardware designer. All you need to do is get a list of chip-select settings from him and load those settings into the chip-select registers.

Upon reset, the 80188EB assumes a worst-case scenario. It assumes there are only 1024 bytes of ROM—located in the address range FFC00h to FFFFFh—and that no other memory or I/O devices are present. This is the processor's "fetal position," and it implies that the hw_init routine must be located at address FFC00h (or higher). It must also not require the use of any RAM. The hardware initialization routine should start by initializing the chip-select registers to inform the processor about the other memory and I/O devices that are installed on the board. By the time this task is complete, the entire range of ROM and RAM addresses will be enabled, so the remainder of your software can be located at any convenient address in either ROM or RAM.

The third initialization stage contains the startup code. This is the assembly-language code that we saw back in Chapter 3. In case you don't remember, its job is to the prepare the way for code written in a high-level language. Of importance here is only that the startup code calls main. From that point forward, all of your other software can be written in C or C++.

Hopefully, you are starting to understand how embedded software gets from processor reset to your main program. Admittedly, the very first time you try to pull all of these components together (reset code, hardware initialization, C/C++ startup code, and application) on a new board there will be problems. So expect to spend some time debugging each of them. Honestly, this will be the hardest part of the project. You will soon see that once you have a working Blinking LED program to fall back on, the work just gets easier and easier—or at least more similar to ordinary computer programming.

Up to this point in the book we have been building the infrastructure for embedded programming. But the topics we're going to talk about in the remaining chapters concern higher-level structures: memory tests, device drivers, operating systems, and actually useful programs. These are pieces of software you've probably seen before on other computer systems projects. However, there will still be some new twists related to the embedded programming environment.

[1]   The toggleLed function wouldn't have required a single line of assembly code if the P2LTCH register had been memory-mapped.

[2]  This situation gets even more complicated if you consider the various memory models provided by some processors. All of the examples in this book assume that the 80188's large memory model is used. In this memory model all of the specifics I'm about to tell you hold for all pointer types. But in the other memory models, the format of the address stored in a pointer differs depending upon the type of code or data pointed to!

[3]  A few processors actually have the first few instructions of the ISR stored there, rather than a pointer to the routine.

[4]   In order to make the example in Chapter 2, a little easier to understand, I didn't show any of the initialization code there. However, it is necessary to get the hardware initialization code working before you can write even simple programs like Blinking LED.